/*
 * @description: 函数
 * @Author: 问道师兄木木木
 * @Date: 2021-02-15 14:27:40
 * @LastEditors: 问道师兄木木木
<<<<<<< HEAD
 * @LastEditTime: 2021-03-02 11:54:13
=======
 * @LastEditTime: 2021-02-28 14:26:16
>>>>>>> 540c0accebb483f784b276786ad6596add7cbad8
 */

 
//! 介绍

//? 函数是JavaScript 应用程序的基础。它帮助你实现抽象层，模拟层，信息隐藏和模块。在TypeScript 里，虽然已经支持类，
//? 命名空间和模块，但函数仍然是主要的定义 行为的地方。 TypeScript 为 JavaScript 函数添加了额外的功能，
//? 让我们可以更容易地使用。


//! 函数

//? 和JavaScript 一样， TypeScript 函数可以创建有名字的函数和匿名函数。你可以随意选择合适应用程序的方式，
//? 不论是定义一系列 API 函数还是只使用一次的函数。

//? 通过下面的例子可以迅速回想起这两种 JavaScript 中的函数：


//* 有名字的函数
// function add(x , y) {
//     return x + y;
// }


//* 匿名函数
// let myAdd = function(x, y) { return x + y; }

//? 在 JavaScript 里，函数可以使用函数体外部的变量。当函数这么做时，我们说它 '捕获' 了这些变量。
//? 至于为什么可以这样做以及其中利弊超出了文本的范围（闭包），但深刻理解这个机制对学习JavaScript 和 TypeScript 会很有帮助

let z = 100;

function addToZ(x: number, y: number) {
    return x + y + z;
}



//! 函数类型

//* 为函数定义类型
//? 让我们为上面那个函数添加类型：

function add(x: number, y:number): number {
    return x + y
}

let myAdd1 = function(x: number, y: number): number { return x + y; };

console.log(myAdd1(1,2));

//? 我们可以给每一个参数添加类型之后再为函数本身添加返回值类型。TypeScript 能够根据返回语句自动推断出返回值类型
//? 因此我们通常省略它。

//* 书写完整函数类型
//? 现在我们已经为函数指定了类型，下面让我们写出函数的完整类型

let myAdd2: (x: number, y: number) => number = function(x: number, y: number): number { return x + y; };

//? 函数类型包含两部分：参数类型和返回值类型。当写出完整函数类型的时候，这两部分都是需要的。我们可以参照列表
//? 的形式写出参数类型，为每个参数指定一个名字和类型。这个名字只是为了增加可读性。我们也可以这么写：


let myAdd3: (baseValue: number, increment: number) => number =  function(x: number, y: number): number { return x + y; };

//? 只要参数类型是匹配的，那么就认为它是有效的参数类型，而不在乎参数名是否正确。

//? 第二部分是返回值类型。对于返回值，我们在函数和返回值之前使用 ( => ) 符号，使之清晰明了
//? 如果之前提到的，返回值类型是函数类型的必要部分，如果函数没有返回任何值，你也必须要指定类型为 void 而不能留空

//* 推断类型

//? 尝试这个例子的时候，你会返现如果你在赋值语句的一遍指定了类型但是另一边没有类型的话， TypeScript 编译器会自动识别出来

let myAdd4 = function(x: number, y: number): number {return x + y};

let myAdd5: (baseValue: number, increment: number) => number = function(x, y){ return x + y }

//? 这叫做 ”按上下文归类“，是类型推论的一种。它帮助我们更好地为程序指定类型。


//* 可选参数和默认参数

//? TypeScript 里的每一个函数参数都是必须的。这不是指不能传递 null 或 undefined 作为参数，而是说编译器检查用户
//? 是否为每个参数都传了值。编译器还会假设只有这些参数会被传递进函数。简短地说，传递给一个函数的参数个数必须与函数期望的参数个数一致

function buildName(firstName: string, lastName: string) {
    return `${firstName} ${lastName}`
}

// let result1 = buildName("Bob");  /** error, too few parameters */
// let result2 = buildName("Bob", "Adams", "Sr.");   /** error, too many parameters */
let result3 = buildName("Bob", "Adams");


//? JavaScript 里，每个参数都是可选的，可传可不传。没传参的时候，它的值就是 undefined。在 TypeScript 里我们可以
//? 在参数名旁使用 ? 实现可选参数的功能。比如，我们想让last name 是可选的：

function buildName1(firstName: string, lastName?: string) {
    if(lastName) return `${firstName} ${lastName}`;
    else return firstName;
}

let result4 = buildName1("Bob"); /** works correctly now */
// let result5 = buildName1("Bob", "Adams", "Sr."); /** error, too many parameters */
let result6 = buildName1("Bob", "Adams"); /** ah, just right */

//? 可选参数必须跟在必须参数后面。如果上例我们想让 firstName 是可选的，那么就必须调整它们的位置，把firstName 放在后面

//? 在TypeScript 里，我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是 undefined 时。它们叫做 有默认初始化的参数
//? 让我们修改上例，把lastName 的默认值设置为 "Smith"。

function buildName2(firstName: string, lastName: string = "Smith" ) {
    return `${firstName} ${lastName}`
}

let result7 = buildName2("Bob");   /** works correctly now, returns "Bob Smith" */
let result8 = buildName2("Bob", undefined);   /** still woks, also return "Bob Smith" */
// let result9 = buildName2("Bob", "Adams", "Sr."); /** error, too many parameters */ 
let result10 = buildName2("Bob", "Adams");   /** ah, just right */

//? 在所有必须参数后面的 带默认初始化的参数都是可选的。与可选参数一样，在调用函数的时候可以省略。也就是说
//? 可选参数与末尾的 默认参数共享参数类型。
function buildName3(firstName: string, lastName?: string) {
    // ...
}

//? 和

function buildName4(firstName: string, lastName = "Smith") {
    // ...
}

//? 共享同样的类型 (firstName: string, lastName?: string) => string。 默认参数的默认值消失了，只保留了
//? 它时一个可选参数的信息。

//? 与普通可选参数不同的是，带默认值的参数不需要放在必须参数的后面。如果带默认值的参数出现在必须参数前面，
//? 用户必须明确的传入 Undefined 值来获得默认值。例如，我们重写最后一个例子，让 firstName 是带默认值的参数：
function buildName5(firstName = "Will", lastName: string) {
    return `${firstName} ${lastName}`
}

// let result11 = buildName5("Bob");    /** error, too few parameter */
// let result12 = buildName5("Bob", "Adams", "Sr.");   /** error, too many parameters */
let result13 = buildName5("Bob", "Adams");   /** okay and returns "Bob Adams" */
let result14 = buildName5(undefined, "Adams");   /** okay and returns "Will Adams" */


//* 剩余参数 

//? 必要参数，默认参数和可选参数有个共同点： 它们表示某一个参数。有时，你想同时操作多个参数，或者你并不知道会有
//? 多少个参数传递进来。在 JavaScript 里，你可以使用 arguments 来访问所有传入的参数。

//? 在 TypeScript 里，你可以把所有参数收集到一个变量里:

function buildName6(firstName: string, ...restOfName: string[]) {
    return `${firstName} ${restOfName.join("")}`
}

let employeeName = buildName6("Joseph", "Samuel", "Lucas", "MacKinzie");

//? 剩余参数会被当做个数不限的可选参数。可以一个都没有，同样也可以有任意个。编译器创建参数数组，名字是你在省略号 (...) 
//? 后面给定的名字，你可以在函数体内使用这个数组。

//? 这个省略号也会在带有剩余参数的函数类型定义上使用到:
function buildName7(firstName: string, ...restOfName: string[]) {
    return `${firstName} ${restOfName.join("")}`
}

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName7;



//! this
//? 学习如何在JavaScript 里正确使用 this 就好比一场成年礼。由于TypeScript 是 JavaScript 的超集
//? TypeScript程序员也需要弄清 this 工作机制并且当有 bug 的时候能够找出错误所在。幸运的是，TypeScript 能够通知你错误地使用了 this 的地方

//* this 和 箭头函数
//? JavaScript 里，this 的值在函数被调用的时候才会指定。这是个即强大又灵活的特点，但是你需要花点时间弄清楚出函数用的上下文是什么。
//? 但众所周知，这不是一件很简单的事，尤其是在返回一个函数或将函数当做参数传递的时候。

//? 下面看一个例子:
let deck = { 
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function(){
        return function (){ 
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

              return {suit:/**  this.suits[pickedSuit] */ '1' , card: pickedCard % 13};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert(`card: ${pickedCard.card} of ${pickedCard.suit} `);

//? 可以看到 createCardPicker 是个函数，并且它又返回了一个函数。如果我们尝试运行这个程序，会发现他并没有弹出对话框
//? 而是报错了。因为 createCardPicker 返回的函数里的 this 被设置成了 window 而不是deck 对象。
//? 因为我们只是独立的调用 cardPicker()。顶级的非方法式调用会将 this 视为 window。（注意：在严格模式下，
//? this 为 undefined 而不是 window)

//? 为了解决这个问题，我们可以在函数被返回时就绑好正确的this。这样的话，无论之后怎么使用它，都会引用绑定的 'deck' 对象。
//? 我们需要改变函数表达式来使用 ECMAScript 6箭头语法。箭头函数能保持函数创建时的 this 值 ，而不是调用时的值

let deck1 = {
    suits: ['hearts', 'spades', 'clubs', 'diamonds'],
    cards: Array(52),
    createCardPicker: function (){
        // NOte: the line below is now an arrow function, allowing us to capture 'this' right here
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

let cardPicker1 = deck1.createCardPicker();
let pickedCard11 = cardPicker1();

alert(`card: ${pickedCard11.card} of ${pickedCard11.suit}`);

//? 更好事情是，TypeScript 会警告你犯了一个错误，如果你给编译器设置了 --noImplicitThis 标识。
//? 它会指出 this.suit[pickedSuit] 里的 this的类型为 any。这是因为 this 来自对象字面量里的表达式。
//? 修改的方法是，提供一个显式的 this 参数。this 参数是个假的参数，它出现在参数列表的最前面：

function f(this: void) {
    // make sure `this` is unusable in this standalone function
}

//? 让我们往例子里添加一些接口, Card 和 Deck , 让类型重用能够变得清晰简单些：
interface Card {
    suit: string;
    card: number;
}

interface Deck {
    suits: string[];
    cards: number[];
    createCardPicker(this: Deck): () => Card;
}

let deck2: Deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    // NOTE: The function now explicitly specifies that its callee must be of type Deck
    createCardPicker: function(this: Deck) {
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13}; 
        }
    }
}

let cardPicker2 = deck2.createCardPicker();
let pickedCard23 = cardPicker2();

console.log(`card: ${pickedCard23.card} of ${pickedCard23.suit}`);

//? 现在 TypeScript 知道 createCardPicker 期望在某个 Deck对象上调用.也就是说 this是 Deck类型的，
//? 而非 any, 因此 --noImplicitThis 不会报错了.

//* this 参数在回调函数里
//? 你可以也看到过在回调函数里的this 报错，当你将一个函数传递到某个库函数里稍后会被调用时。因为当回调
//? 被调用的时候，它们会被当成一个普通函数调用，this 将为 undefined 。 稍做改动，你就可以通过 this 参数来
//? 避免错误。首先，库函数的作者要指定 this的类型

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

interface Event {
    message: string;
}

//? this: void means that add ClickListener expects onclick to be a function that does not require a this
//? type. Second, annotate your calling code with this:

class Handler {
    info: string;
    onClickBad(this: Handler, e: Event) {
        //  oops, used this here. using this callback would crash at runtime
        this.info = e.message;
    }
}

let h = new Handler();
// uiElement.addClickListener(h.onClickBad.onClickBad); /** error! */

//? 指定了 this 类型后，你显式声明 onClickBad 必须在 Handler的实例上调用。然后 TypeScript 会检测到
//? addClickListener 要求函数带有 this: void。改变 this 类型来修改这个错误：
class Handler1 {
    info: string;
    onClickGood(this: void, e: Event) {
        // can't use this hers because it's of type void!
        console.log('clicked!');
    }
}

let h1 = new Handler1();
// uiElement.addClickListener(h1.onClickGood);

//? 因为 onClickGood 指定了 this 类型为 void，因此传递 addClickListener 是合法的。当然了，这也意味着不能
//? 使用 this.info 如果你两者都想要，你不得不使用箭头函数了：

class Handel2 {
    info: string;
    onClickGood = (e: Event) => { this.info = e.message; }
}

//? 这是可行的因为箭头函数不会捕获 this，所以你总是可以把它们传给期望 this: void 的函数。缺点是每个
//? Handler 对象都会创建一个箭头函数。另一方面，方法只会被创建一次，添加 Handler 的原型链上。它们在不
//? 同 Handler 对象间是共享的。


//! 重载

//? JavaScript 本身是个动态语言。JavaScript 里函数根据传入不同的参数而返回不同类型的数据是很常见的。

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: any): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if(typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    
    // Otherwise just let them pick the card
    else if(typeof x == "number") {
        let pickedCard = Math.floor(x / 13);
        return { suit: suits[pickedCard], card: x % 13};
    }
}

let myDeck = [{ suit: "diamonds", card: 2}, {suit: "spades", card: 10}, {suit: "hearts", card: 4}];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert(`card: ${ pickedCard1.card } of ${pickedCard1.suit}`);

let pickedCard2 = myDeck[pickCard(15)];
alert(`card: ${ pickedCard2.card } of ${pickedCard2.suit}`);

//? pickCard 方法根据传入参数的不同会返回两种不同类型。 如果传入的是代表纸牌的对象，函数作用是从中抓
//? 一张牌。如果用户想抓牌，我们告诉他抓到了什么牌。但是这怎么在类型系统里表示呢。

//? 方法是为同一个函数提供多个函数类型定义进行函数重载。编译器会根据指这个列表去处理这个函数的调用。
//? 下面我们来重载 pickCard 函数。

let suit1 = ["hearts", "spades", "clubs", "diamonds"];

function pickCard1(x: {suit: string;card: number; }[]): number;
function pickCard1(x: number): {suit: string; card: number; };
function pickCard1(x: any): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if( typeof x == "object" ){
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if ( typeof x == "number" ) {
        let pickedSuit = Math.floor( x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck1 = [{ suit: "diamonds", card: 2}, { suit: "spades", card: 10}, { suit: "hearts", card: 4}];
let pickedCard3 = myDeck1[pickCard1(myDeck1)];
alert(`card：${ pickedCard3.card } of ${ pickedCard3.suit }`);

let pickedCard4 = pickCard1(15);
alert(`card：${ pickedCard4.card } of ${ pickedCard4.suit }`);

//? 这样改变后，重载的 pickCard 函数在调用的时候会进行正确的类型检查

//? 为了让编译器能够选择正确的检查类型，它与JavaScript 里的处理流程相似。它查找重载列表，尝试使用第一个重载
//? 定义。如果匹配的话就使用这个。 因此，在定义重载的时候，一定要被最精确的定义放在最前面。

//? 注意， function pickCard(x): any 并不是重载列表的一部分，因此这里只有两个重载： 一个是接受对象另一个是接受数字
//? 以其他参数调用 pickCard 会产生错误。
